/*******************************************************************************
 * Copyright (c) 2007 University of Oslo.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     University of Oslo - initial API and implementation
 *******************************************************************************/

package com.cea.papyrus.diagram.sequence.sedi.edit;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.EditPart;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.InteractionOperand;
import org.eclipse.uml2.uml.Lifeline;
import org.eclipse.uml2.uml.Message;
import org.eclipse.uml2.uml.MessageEnd;
import org.eclipse.uml2.uml.NamedElement;
import org.eclipse.uml2.uml.UMLPackage;

import com.cea.papyrus.core.utils.LookForElement;
import com.cea.papyrus.diagram.sequence.sedi.edit.handlers.LifelineLinkHandler;
import com.cea.papyrus.diagram.sequence.sedi.edit.handlers.SeparatorLinkHandler;
import com.cea.papyrus.diagram.sequence.sedi.editor.message.MessageUtil;
import com.cea.papyrus.diagram.sequence.sedi.model.command.diagram.HintRegister;
import com.cea.papyrus.diagram.sequence.sedi.model.util.ModelUtil;
import com.cea.papyrus.diagraminterchange2.di2.Di2Factory;
import com.cea.papyrus.diagraminterchange2.di2.Diagram;
import com.cea.papyrus.diagraminterchange2.di2.GraphEdge;
import com.cea.papyrus.diagraminterchange2.di2.GraphElement;
import com.cea.papyrus.diagraminterchange2.di2.GraphNode;
import com.cea.papyrus.diagraminterchange2.di2.Uml1SemanticModelBridge;

/**
 * Creates diagram elements to represent the UML model elements.
 * 
 * @author frankd@ifi.uio.no
 */
public class SeDiViewFactory {

    /*
     * There are basically two kinds of diagram elements ("views"): nodes and links. Each
     * one represents some element in the UML model. There is no guarantee about the order
     * of creation. In fact, it is practically guaranteed that some links will be created
     * before all nodes have been created. This means that links might want to link to
     * nodes that not yet exist. In fact, It might even mean that links don't yet know who
     * they will link to (say, for example, that a blank message has just been
     * instantiated). To handle these problems, we use a couple of techniques. When the
     * link knows who it wants to link to, but that node has not yet been created, we use
     * a "delayed linker". The link will associate a Linker with the underlying UML
     * element. Then, when the node is eventually created, it will check for any linkers
     * associated with its UML element and run them. When the link is a message that
     * doesn't yet know who it want to connect to, we install a "placeholder" adapter on
     * the message itself, temporarily representing the real message end. This placeholder
     * adapter listens for assignments to the message ends. When a message end is finally
     * assigned, it will associate the link with the node corresponding to the message
     * end.
     */

    // --- 1 Delayed Linker ---
    private static interface Linker {
        void link(GraphNode element);
    }

    // oh how I long for MultiMaps in Java
    private static Map<Element, List<Linker>> delayedLinkers = new HashMap<Element, List<Linker>>();

    private static void addDelayedLinker(Element element, Linker linker) {
        List<Linker> linkers = delayedLinkers.get(element);
        if (linkers == null) {
            linkers = new ArrayList<Linker>(2);
            delayedLinkers.put(element, linkers);
        }
        linkers.add(linker);
    }

    private static List<Linker> getDelayedLinkers(Element element) {
        List<Linker> linkers = delayedLinkers.remove(element);
        if (linkers == null)
            linkers = Collections.emptyList();
        return linkers;
    }

    // --- 2 MessageEnd Placeholder ---

    @SuppressWarnings("unchecked")
    public static void installMessageEndPlaceholder(final Diagram diagram, final GraphEdge link,
            final boolean send, final String anchorHint) {
        ModelUtil.<Element> getSemantic(link).eAdapters().add(
                new LinkAdapter(diagram, link, send, anchorHint));
    }

    private static class LinkAdapter extends AdapterImpl {
        private final Diagram diagram;
        private final GraphEdge link;
        private final String anchorHint;
        /**
         * If true, this adapter monitors sender changes; otherwise it monitors receiver
         * changes.
         */
        private final boolean watchSendEnd;

        public LinkAdapter(final Diagram diagram, final GraphEdge link, final boolean watchSendEnd,
                final String anchorHint) {
            this.diagram = diagram;
            this.link = link;
            this.watchSendEnd = watchSendEnd;
            this.anchorHint = anchorHint;
        }

        public void notifyChanged(Notification evt) {
            switch (evt.getFeatureID(UMLPackage.class)) {
            case UMLPackage.MESSAGE__SEND_EVENT:
                if (watchSendEnd && evt.getOldValue() == null && evt.getNewValue() != null) {
                    setMessageSender(diagram, link, anchorHint);
                    ModelUtil.<EObject> getSemantic(link).eAdapters().remove(this);
                }
                break;
            case UMLPackage.MESSAGE__RECEIVE_EVENT:
                if (!watchSendEnd && evt.getOldValue() == null && evt.getNewValue() != null) {
                    setMessageReceiver(diagram, link, anchorHint);
                    ModelUtil.<EObject> getSemantic(link).eAdapters().remove(this);
                }
                break;
            }
        }

        public boolean isAdapterForType(Object type) {
            return type instanceof Message;
        }
    }

    // --- Public services ---

    /**
     * Creates a view element (either a GraphicalElement or a LinkElement) for the given
     * UML element.
     * 
     * @param diagram
     *            The diagram which will contain the view.
     * @param umlElement
     *            The UML element that the view will represent.
     * @param anyEditPart
     *            A reference to any EditPart. EditParts provide access to the
     *            EditPartRegistry, which allows the factory to find the editpart for any
     *            element. This is sometimes necessary because some views require detailed
     *            information about other views - information that is only available
     *            through the editparts.
     * @return the created view element.
     */
    public static GraphElement createView(final GraphElement parent, final Element umlElement,
            final EditPart anyEditPart) {
        if (umlElement instanceof Message)
            return createMessageView(parent, (Message) umlElement, anyEditPart);
        else if (umlElement instanceof InteractionOperand)
            return createSeparatorView(parent, (InteractionOperand) umlElement);
        else if (umlElement instanceof NamedElement)
            return createGraphicalElement(parent, (NamedElement) umlElement, anyEditPart);
        else
            return createGraphicalElement2(parent, umlElement);
    }

    /**
     * Creates a GraphicalElement view for the given model element. TODO delegate the
     * graphical element creation to a handler
     */
    @SuppressWarnings("unchecked")
    private static GraphElement createGraphicalElement(final GraphElement parent,
            final Element element, final EditPart anyEditPart) {
        GraphNode newView = Di2Factory.eINSTANCE.createGraphNode();
        Uml1SemanticModelBridge semanticModelBridge =
                Di2Factory.eINSTANCE.createUml1SemanticModelBridge();
        semanticModelBridge.setElement(element);
        semanticModelBridge.setGraphElement(newView);
        newView.setSemanticModel(semanticModelBridge);

        Rectangle bounds = (Rectangle) HintRegister.consumeHint("bounds");
        Diagram diagram = LookForElement.getDiagram(parent);
        if (bounds == null) {
            return null;
            // if (element instanceof Interaction)
            // bounds = new InteractionLayoutHandler().layout(diagram,
            // (Interaction) element);
            // else if (element instanceof Lifeline)
            // bounds = new LifelineLayoutHandler().layout(diagram,
            // (Lifeline) element);
            // else if (element instanceof InteractionUse)
            // bounds = new InteractionUseLayoutHandler().layout(diagram,
            // (InteractionUse) element);
            // else if (element instanceof StateInvariant)
            // bounds = new StateInvariantLayoutHandler().layout(diagram,
            // (StateInvariant) element);
            // else if (element instanceof Continuation)
            // bounds = new ContinuationLayoutHandler().layout(diagram,
            // (Continuation) element);
            // else if (element instanceof ExecutionSpecification)
            // bounds = new ExecutionSpecificationLayoutHandler().layout(
            // diagram, (ExecutionSpecification) element);
            // else if (element instanceof InteractionConstraint)
            // bounds = new GuardLayoutHandler().layout(diagram,
            // (InteractionConstraint) element, anyEditPart);
            // else if (element instanceof Comment)
            // bounds = new CommentLayoutHandler().layout(diagram,
            // (Comment) element);
            // else
            // // TODO Ask LayoutHandler for default/recommended bounds
            // // for other element types
            // // As a last resort, assign some default bounds
            // bounds = new Rectangle(100, 100, 100, 100);
        }
        newView.setSize(bounds.getSize());
        newView.setPosition(bounds.getLocation());
        parent.getContained().add(newView);
        // handle any delayed linking involving this element
        for (Linker linker : getDelayedLinkers(element))
            linker.link(newView);

        return newView;
    }

    /**
     * Creates a GraphicalElement view for the given model element. TODO delegate the
     * graphical element creation to a handler
     */
    @SuppressWarnings("unchecked")
    private static GraphElement createGraphicalElement2(final GraphElement parent,
            final Element element) {
        return createGraphicalElement(parent, element, null);
        // GraphicalElement2 newView =
        // ModelFactory.eINSTANCE.createGraphicalElement2();
        // newView.setElement(element);
        // diagram.getContents().add(newView);
        //
        // Rectangle bounds = (Rectangle) HintRegister.consumeHint("bounds");
        // if (bounds == null) {
        // if (element instanceof Comment)
        // bounds = new CommentLayoutHandler().layout(diagram, (Comment)
        // element);
        // else
        // // TODO Ask LayoutHandler for default/recommended bounds
        // // for other element types
        // // As a last resort, assign some default bounds
        // bounds = new Rectangle(100, 100, 100, 100);
        // }
        // newView.setBounds(bounds);
        //
        // // delayed linking is not currently necessary because no Elements
        // // send or receive messages
        // // handle any delayed linking involving this element
        // // for (Linker linker : getDelayedLinkers(element))
        // // linker.link(newView);
        //
        // return newView;
    }

    @SuppressWarnings("unchecked")
    private static GraphElement createMessageView(final GraphElement parent, final Message message,
            EditPart anyEditPart) {
        final GraphEdge messageView = Di2Factory.eINSTANCE.createGraphEdge();
        Uml1SemanticModelBridge semanticModelBridge =
                Di2Factory.eINSTANCE.createUml1SemanticModelBridge();
        semanticModelBridge.setElement(message);
        semanticModelBridge.setGraphElement(messageView);
        messageView.setSemanticModel(semanticModelBridge);
        parent.getContained().add(messageView);
        messageView.setContainer(parent);

        Diagram diagram = LookForElement.getDiagram(parent);
        setMessageSender(diagram, messageView, (String) HintRegister.consumeHint("source"));
        setMessageReceiver(diagram, messageView, (String) HintRegister.consumeHint("target"));

        //
        // Updates Anchor position :

        return messageView;
    }

    @SuppressWarnings("unchecked")
    private static GraphElement createSeparatorView(final GraphElement parent,
            final InteractionOperand operand) {
        final GraphEdge separatorView = Di2Factory.eINSTANCE.createGraphEdge();
        Uml1SemanticModelBridge semanticModelBridge =
                Di2Factory.eINSTANCE.createUml1SemanticModelBridge();
        semanticModelBridge.setElement(operand);
        semanticModelBridge.setGraphElement(separatorView);
        separatorView.setSemanticModel(semanticModelBridge);
        parent.getContained().add(separatorView);
        Diagram diagram = LookForElement.getDiagram(parent);
        setSeparatorEnds(diagram, separatorView, (String) HintRegister.consumeHint("source"),
                (String) HintRegister.consumeHint("target"));
        return separatorView;
    }

    private static void setMessageSender(final Diagram diagram, final GraphEdge messageView,
            final String anchorHint) {

        final Message message = ModelUtil.<Message> getSemantic(messageView);
        final NamedElement sender = MessageUtil.getSender(message);
        if (sender != null) {
            final MessageEnd sendEvent = message.getSendEvent();
            final GraphElement senderView = ModelUtil.getGraphElement(sender, diagram);
            if (senderView != null) {
                ModelUtil.setSourceRef(messageView, selectAnchor((GraphNode) senderView, sendEvent,
                        anchorHint));
                messageView.setAnchorSource(senderView);

            } else {
                addDelayedLinker(sender, new Linker() {
                    public void link(GraphNode view) {
                        final GraphNode senderView = ModelUtil.getGraphNode(sender, diagram);
                        if (ModelUtil.getSourceRef(messageView) == null) {
                            ModelUtil.setSourceRef(messageView, selectAnchor(
                                    (GraphNode) senderView, sendEvent, anchorHint));
                        }
                        messageView.setAnchorSource(view);
                    }
                });
            }
        } else
            installMessageEndPlaceholder(diagram, messageView, true, anchorHint);
    }

    private static void setMessageReceiver(final Diagram diagram, final GraphEdge messageView,
            final String anchorHint) {

        final Message message = ModelUtil.<Message> getSemantic(messageView);
        final NamedElement receiver = MessageUtil.getReceiver(message);
        if (receiver != null) {
            final MessageEnd receiveEvent = message.getReceiveEvent();
            final GraphElement receiverView =
                    (GraphElement) ModelUtil.getGraphElement(receiver, diagram);
            if (receiverView != null) {
                ModelUtil.setTargetRef(messageView, selectAnchor((GraphNode) receiverView,
                        receiveEvent, anchorHint));
                HintRegister.setHint("target", anchorHint);
                messageView.setAnchorTarget(receiverView);
            } else {
                addDelayedLinker(receiver, new Linker() {
                    public void link(GraphNode view) {
                        final GraphNode receiverView = ModelUtil.getGraphNode(receiver, diagram);
                        if (ModelUtil.getTargetRef(messageView) == null) {
                            ModelUtil.setTargetRef(messageView, selectAnchor(receiverView,
                                    receiveEvent, anchorHint));
                        }
                        messageView.setAnchorTarget(view);
                    }
                });
            }
        } else
            installMessageEndPlaceholder(diagram, messageView, false, anchorHint);
    }

    private static void setSeparatorEnds(final Diagram diagram, final GraphEdge separatorView,
            final String anchorHintLeft, final String anchorHintRight) {

        final InteractionOperand operand =
                ModelUtil.<InteractionOperand> getSemantic(separatorView);
        final NamedElement cfragment = (NamedElement) operand.getOwner();
        final GraphNode cfragmentView = ModelUtil.getGraphNode(cfragment, diagram);

        ModelUtil.setSourceRef(separatorView, selectAnchor(cfragmentView, operand, anchorHintLeft,
                true));
        ModelUtil.setTargetRef(separatorView, selectAnchor(cfragmentView, operand, anchorHintRight,
                false));
        separatorView.setAnchorSource(cfragmentView);
        separatorView.setAnchorTarget(cfragmentView);
    }

    // TODO Refactor
    // This method is also present in MessageEditPart to assist responding to
    // message movement.
    @SuppressWarnings("unchecked")
    private static String selectAnchor(final GraphNode view, final MessageEnd event,
            final String anchorHint) {
        String anchorRef = anchorHint;
        if (anchorHint == null) {
            if (ModelUtil.<NamedElement> getSemantic(view) instanceof Lifeline)
                anchorRef = new LifelineLinkHandler().selectAnchor(view, event);
        }
        assert anchorRef != null;
        return anchorRef;
    }

    // TODO Refactor - if the method above can be improved, then this one can as
    // well
    @SuppressWarnings("unchecked")
    private static String selectAnchor(final GraphNode view, final InteractionOperand operand,
            final String anchorHint, final boolean leftSide) {
        String anchorRef = anchorHint;
        if (anchorHint == null)
            anchorRef = new SeparatorLinkHandler().selectAnchor(view, operand, leftSide);
        assert anchorRef != null;
        return anchorRef;
    }

}
